﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace MvcDeveloper.Infrastructure.Routing
{
    /// <summary>
    /// This implementation of RouteBase is used for presenting legacy help file to the user as an MVC route.
    /// 
    /// <para>
    /// The parameters to this constructor are as following:
    /// </para>
    /// <para>
    /// - url - This is an MVC-style URL, like {controller}/{action}/{topic}
    /// </para>
    /// <para>
    /// - legacyUrl - This is a legacy style URL like ~/Contents/{filename}.aspx
    /// </para>
    /// <para>
    /// - routeHandler - This is MvcRouteHandler instance by default
    /// </para>
    /// <para>
    /// When the request arrives, and which can be served by HelpRoute, we first examine whether this is the case of
    /// a legacy URL (certain links in documents could place such a request) or whether it is an MVC style request.
    /// Based on this information we shall parse the route and build the RouteData object that we return back to 
    /// the routing system.
    /// </para>
    /// <para>
    /// For more information please see: 
    /// http://mvcdeveloper.wordpress.com/2013/05/31/using-legacy-urls-in-mvc-via-custom-routebase/
    /// </para>
    /// </summary>
    public class HelpRoute : RouteBase
    {
        private readonly IRouteHandler _routeHandler;
        private RouteValueDictionary _urlSegments;
        
        /// <summary>
        /// Initializes the HelpRoute object by allowing user to customize every part of this RouteBase implementation.
        /// </summary>
        /// <param name="url">MVC-style route template like {controller}/{action}/{id}</param>
        /// <param name="legacyUrl">Legacy URL template like /LegacyContent/{filename}.html (</param>
        /// <param name="routeHandler">
        /// Implementation of IRouteHandler to use. 
        /// If this argument is null MvcRouteHandler is used by default.
        /// </param>
        public HelpRoute(string url, string legacyUrl, IRouteHandler routeHandler)
        {
            if (String.IsNullOrWhiteSpace(url))
            {
                throw new ArgumentNullException("url");
            }

            if (String.IsNullOrWhiteSpace(legacyUrl))
            {
                throw new ArgumentNullException("legacyUrl");
            }
            
            if (!legacyUrl.Contains("~"))
            {
                legacyUrl = legacyUrl.Insert(0, "~");
            }
            
            Url = url;
            LegacyUrl = legacyUrl;
            _routeHandler = routeHandler ?? new MvcRouteHandler();
            _urlSegments = new RouteValueDictionary();

            InitializeUrlSegments();
        }

        /// <summary>
        /// Initializes Url pattern so that we can use the values it contains
        /// later when we build RouteData object.
        /// </summary>
        /// <remarks>
        /// {controller}/{action}/{topic}
        /// 
        /// </remarks>
        private void InitializeUrlSegments()
        {
            var urlArray = (IList<string>)Url.Split(new[] {"/"}, StringSplitOptions.RemoveEmptyEntries);

            _urlSegments.Add("controller", urlArray[0]);
            _urlSegments.Add("action", urlArray[1]);
            _urlSegments.Add("topic", String.Empty);
        }

        /// <summary>
        /// Initializes the HelpRoute object with user's value. The RouteHandler used is MvcRouteHandler.
        /// </summary>
        /// <param name="url">MVC-style route template like {controller}/{action}/{id}</param>
        /// <param name="legacyUrl">Legacy URL template like ~/LegacyContent/{filename}.html</param>
        public HelpRoute(string url, string legacyUrl)
            : this(url, legacyUrl, new MvcRouteHandler())
        {
        }

        public string Url { get; private set; }
        public string LegacyUrl { get; private set; }
        
        /// <summary>
        /// When overridden in a derived class, returns route information about the request.
        /// </summary>
        /// <returns>
        /// An object that contains the values from the route definition if the route matches the current request, 
        /// or null if the route does not match the request.
        /// </returns>
        /// <param name="httpContext">An object that encapsulates information about the HTTP request.</param>
        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            RouteData routeData;
            var requestedUrl = httpContext.Request.AppRelativeCurrentExecutionFilePath;

            if (IsLegacyUrl(requestedUrl))
            {
                routeData = BuildFromLegacyUrl(requestedUrl);
            }
            else
            {
                routeData = BuildFromMvcUrl(requestedUrl);
            }

            return routeData;
        }

        /// <summary>
        /// When overridden in a derived class, checks whether the route matches the specified values, 
        /// and if so, generates a URL and retrieves information about the route.
        /// </summary>
        /// <returns>
        /// An object that contains the generated URL and information about the route, 
        /// or null if the route does not match <paramref name="values"/>.
        /// </returns>
        /// <param name="requestContext">An object that encapsulates information about the requested route.</param>
        /// <param name="values">An object that contains the parameters for a route.</param>
        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
        {
            var virtualPathData = (VirtualPathData) null;

            if (values.ContainsKey("topic"))
            {
                var urlHelper = new UrlHelper(requestContext);
                var content = urlHelper.Content(values.GenerateMvcUrl("topic"));
                virtualPathData = new VirtualPathData(this, content);
            }

            return virtualPathData;
        }

        /// <summary>
        /// We are basically just doing a simple check for .html extension.
        /// </summary>
        /// <param name="requestedUrl">Requested URL to check</param>
        /// <returns>True if this is a legacy URL, otherwise false.</returns>
        private bool IsLegacyUrl(string requestedUrl)
        {
            return requestedUrl.ToLower().Contains(".html");
        }

        /// <summary>
        /// Extracts the information from the legacy URL prior to building a RouteData object.
        /// </summary>
        /// <param name="requestedUrl">URL to process and the parameter to extract</param>
        /// <returns>Returns RouteData object or null.</returns>
        private RouteData BuildFromLegacyUrl(string requestedUrl)
        {
            var routeData = (RouteData) null;

            if (IsLegacyTemplateMatch(requestedUrl))
            {
                routeData = BuildRouteData(requestedUrl);
            }

            return routeData;
        }

        /// <summary>
        /// Builds a RouteData from standard MVC URL by calling BuildRouteData method.
        /// </summary>
        /// <param name="requestedUrl">Request URL to process.</param>
        /// <returns>Returns RouteData object or null.</returns>
        private RouteData BuildFromMvcUrl(string requestedUrl)
        {
            var routeData = (RouteData) null;
            if (requestedUrl.ToLower().Contains(((string)_urlSegments["controller"]).ToLower()))
            {
                routeData = BuildRouteData(requestedUrl);
            }
            
            return routeData;
        }

        /// <summary>
        /// Checks whether requested URL matches the legacy URL pattern. If there is a {filename} placeholder,
        /// the page file name is used without extension.
        /// </summary>
        /// <param name="requestedUrl">Requested URL</param>
        /// <returns>True if there is a match, otherwise false.</returns>
        private bool IsLegacyTemplateMatch(string requestedUrl)
        {
            var lastIndex = requestedUrl.LastIndexOf("/", StringComparison.Ordinal);
            var requestedUrlFolder = requestedUrl.Substring(0, lastIndex).ToLower();
            var requestedUrlFile = requestedUrl.Substring(lastIndex + 1);

            if (LegacyUrl.ToLower().Contains(requestedUrlFolder))
            {
                if (LegacyUrl.Contains("{filename}"))
                {
                    // The first part matches. LegacyUrl folder matches requestedUrl folder.
                    // At this point we discover that there is a {filename} placeholder and
                    // we abandon further comparison. We return true. The rest of the process
                    // can continue.
                    return true; 
                }
                
                // Here we check if LegacyUrl is registered to serve one particular file.
                // If the result is true then HelpRoute can respond to this request,
                // otherwise another route registration will have to try.
                return LegacyUrl.Contains(requestedUrlFile);
            }

            var possibleMvcUrl = _urlSegments.GenerateMvcUrl();

            // The purpose of this if check is to intercept links that are found on a legacy page
            // that is already shown to the user. For example, if user is presented with /Help/Show/RouteBase,
            // but then clicks on the link at the bottom, this link will be displayed as /Help/Show/GetRouteData.html.
            // At this point, although this is not a path similar to LegacyContent, we still need to present the next
            // page to a user without throwing exception. Basically, it is treated as a continuation 
            // from the current request.
            if (requestedUrl.ToLower().Contains(possibleMvcUrl.ToLower()))
            {
                return true;
            }

            return false; // No match, HelpRoute cannot serve this request
        }

        /// <summary>
        /// Builds RouteData object based on the information retrieved from the request and URL pattern specified.
        /// </summary>
        /// <param name="requestedUrl">Requested URL to parse.</param>
        /// <returns>Returns RouteData object or null.</returns>
        private RouteData BuildRouteData(string requestedUrl)
        {
            var lastIndexOfSlash = requestedUrl.LastIndexOf("/", StringComparison.Ordinal);
            var lastIndexOfDot = requestedUrl.LastIndexOf(".", StringComparison.Ordinal);

            if (lastIndexOfDot == -1)
            {
                // There is no dot. This is MVC URL
                lastIndexOfDot = requestedUrl.Length;
            }

            var fileNameLength = lastIndexOfDot - lastIndexOfSlash - 1;

            var fileName = requestedUrl.Substring(lastIndexOfSlash + 1, fileNameLength);

            _urlSegments["topic"] = fileName;

            var routeData = new RouteData(this, _routeHandler);
            routeData.Values.Add("controller", _urlSegments["controller"]);
            routeData.Values.Add("action", _urlSegments["action"]);
            routeData.Values.Add("topic", _urlSegments["topic"]);

            return routeData;
        }
    }

    internal static class RouteValueDictionaryExtenstion
    {
        public static string GenerateMvcUrl(this RouteValueDictionary values, params string[] urlParameters)
        {
            var url = new StringBuilder();

            if (!(values.ContainsKey("controller") || values.ContainsKey("action")))
            {
                return String.Empty;
            }

            url.AppendFormat("/{0}/{1}", values["controller"], values["action"]);

            foreach (var urlParameter in urlParameters)
            {
                if (values.ContainsKey(urlParameter))
                {
                    url.AppendFormat("/{0}", values[urlParameter]);
                }
            }

            return url.ToString();
        }
    }
}